/* ========================================================= 블룸인베스트 — interactions ========================================================= */ (function () { 'use strict'; var reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; document.addEventListener('DOMContentLoaded', function () { applyContactIcon(); initHeaderScroll(); initReveal(); initStatCounters(); initHeroParallax(); buildReviewStars(); initReviewSlider(); initModals(); }); /* ---- 联系按钮图标 (kakao / band) ---- */ function applyContactIcon() { var src = (typeof CONTACT_ICON !== 'undefined') ? CONTACT_ICON : 'static/images/kakao.png'; document.querySelectorAll('[data-contact-icon]').forEach(function (img) { img.src = src; img.alt = ''; }); } /* ---- 헤더 스크롤 상태 ---- */ function initHeaderScroll() { var header = document.getElementById('siteHeader'); if (!header) return; var onScroll = function () { header.classList.toggle('scrolled', window.scrollY > 12); }; onScroll(); window.addEventListener('scroll', onScroll, { passive: true }); } /* ---- 스크롤 등장 애니메이션 ---- */ function initReveal() { var els = document.querySelectorAll('.reveal, .reveal-up'); if (!('IntersectionObserver' in window) || reduceMotion) { els.forEach(function (el) { el.classList.add('in'); }); return; } var io = new IntersectionObserver(function (entries) { entries.forEach(function (e) { if (e.isIntersecting) { e.target.classList.add('in'); io.unobserve(e.target); } }); }, { threshold: 0.16, rootMargin: '0px 0px -8% 0px' }); els.forEach(function (el) { io.observe(el); }); } /* ---- 통계 카운트업 ---- */ function initStatCounters() { var nums = document.querySelectorAll('.stat-num'); if (!nums.length) return; var run = function (el) { var target = parseFloat(el.getAttribute('data-count')) || 0; var suffix = el.getAttribute('data-suffix') || ''; var isFloat = el.getAttribute('data-float') === '1'; var dur = 1400, start = null; var fmt = function (v) { if (isFloat) return v.toFixed(1); return Math.round(v).toLocaleString('en-US'); }; if (reduceMotion) { el.textContent = fmt(target) + suffix; return; } var step = function (ts) { if (!start) start = ts; var p = Math.min((ts - start) / dur, 1); var eased = 1 - Math.pow(1 - p, 3); el.textContent = fmt(target * eased) + suffix; if (p < 1) requestAnimationFrame(step); else el.textContent = fmt(target) + suffix; }; requestAnimationFrame(step); }; if (!('IntersectionObserver' in window)) { nums.forEach(run); return; } var io = new IntersectionObserver(function (entries) { entries.forEach(function (e) { if (e.isIntersecting) { run(e.target); io.unobserve(e.target); } }); }, { threshold: 0.6 }); nums.forEach(function (el) { io.observe(el); }); } /* ---- 히어로 패럴럭스 / 틸트 ---- */ function initHeroParallax() { var hero = document.getElementById('hero'); var visual = document.getElementById('heroVisual'); if (!hero || !visual || reduceMotion) return; if (!window.matchMedia('(hover: hover) and (pointer: fine)').matches) return; var raf = null, tx = 0, ty = 0; var apply = function () { visual.style.transform = 'translate(' + (tx * 16) + 'px,' + (ty * 11) + 'px) ' + 'rotateY(' + (tx * 7) + 'deg) rotateX(' + (-ty * 6) + 'deg)'; raf = null; }; hero.addEventListener('mousemove', function (e) { var r = hero.getBoundingClientRect(); tx = (e.clientX - r.left) / r.width - 0.5; ty = (e.clientY - r.top) / r.height - 0.5; if (!raf) raf = requestAnimationFrame(apply); }); hero.addEventListener('mouseleave', function () { visual.style.transform = ''; }); } /* ---- 리뷰 별점 타일 생성 ---- */ function buildReviewStars() { document.querySelectorAll('.rc-stars').forEach(function (box) { var filled = box.classList.contains('s4') ? 4 : 5; var html = ''; for (var i = 0; i < 5; i++) { html += ''; } box.innerHTML = html; }); } /* ---- 리뷰 슬라이더 ---- */ function initReviewSlider() { var slider = document.getElementById('reviewsSlider'); var track = document.getElementById('rsTrack'); var prev = document.getElementById('rsPrev'); var next = document.getElementById('rsNext'); var dotsWrap = document.getElementById('rsDots'); if (!slider || !track) return; var cards = Array.prototype.slice.call(track.children); var index = 0, step = 0, perView = 4, maxIndex = 0, autoTimer = null; function measure() { if (!cards.length) return; var gap = parseFloat(getComputedStyle(track).columnGap || getComputedStyle(track).gap) || 20; var cardW = cards[0].getBoundingClientRect().width; step = cardW + gap; var viewW = track.parentElement.getBoundingClientRect().width; perView = Math.max(1, Math.round((viewW + gap) / step)); maxIndex = Math.max(0, cards.length - perView); if (index > maxIndex) index = maxIndex; buildDots(); update(); } function buildDots() { if (!dotsWrap) return; dotsWrap.innerHTML = ''; for (var i = 0; i <= maxIndex; i++) { var b = document.createElement('button'); b.type = 'button'; b.setAttribute('aria-label', (i + 1) + '번째 후기 그룹'); (function (i) { b.addEventListener('click', function () { go(i); }); })(i); dotsWrap.appendChild(b); } } function update() { track.style.transform = 'translateX(' + (-index * step) + 'px)'; if (prev) prev.disabled = index <= 0; if (next) next.disabled = index >= maxIndex; if (dotsWrap) { Array.prototype.forEach.call(dotsWrap.children, function (d, i) { d.classList.toggle('active', i === index); }); } } function go(i) { index = Math.max(0, Math.min(i, maxIndex)); update(); } function nextSlide() { go(index >= maxIndex ? 0 : index + 1); } function prevSlide() { go(index <= 0 ? maxIndex : index - 1); } if (next) next.addEventListener('click', function () { nextSlide(); restartAuto(); }); if (prev) prev.addEventListener('click', function () { prevSlide(); restartAuto(); }); /* autoplay */ function startAuto() { if (reduceMotion) return; autoTimer = setInterval(nextSlide, 4800); } function stopAuto() { if (autoTimer) { clearInterval(autoTimer); autoTimer = null; } } function restartAuto() { stopAuto(); startAuto(); } slider.addEventListener('mouseenter', stopAuto); slider.addEventListener('mouseleave', startAuto); /* swipe / drag */ var sx = 0, dx = 0, dragging = false; function down(x) { dragging = true; sx = x; dx = 0; stopAuto(); track.style.transition = 'none'; } function move(x) { if (!dragging) return; dx = x - sx; track.style.transform = 'translateX(' + (-index * step + dx) + 'px)'; } function up() { if (!dragging) return; dragging = false; track.style.transition = ''; if (Math.abs(dx) > step * 0.22) { dx < 0 ? nextSlide() : prevSlide(); } else update(); startAuto(); } track.addEventListener('touchstart', function (e) { down(e.touches[0].clientX); }, { passive: true }); track.addEventListener('touchmove', function (e) { move(e.touches[0].clientX); }, { passive: true }); track.addEventListener('touchend', up); track.addEventListener('mousedown', function (e) { e.preventDefault(); down(e.clientX); }); window.addEventListener('mousemove', function (e) { move(e.clientX); }); window.addEventListener('mouseup', up); var rt; window.addEventListener('resize', function () { clearTimeout(rt); rt = setTimeout(measure, 150); }); measure(); startAuto(); } /* ---- 약관 / 정책 모달 ---- */ function initModals() { var root = document.getElementById('modalRoot'); if (!root) return; var lastFocus = null; function open(name) { var target = document.getElementById('modal-' + name); if (!target) return; lastFocus = document.activeElement; root.querySelectorAll('.modal').forEach(function (m) { m.classList.remove('is-open'); }); target.classList.add('is-open'); root.classList.add('open'); root.setAttribute('aria-hidden', 'false'); document.body.style.overflow = 'hidden'; var x = target.querySelector('.modal-x'); if (x) x.focus(); } function close() { root.classList.remove('open'); root.setAttribute('aria-hidden', 'true'); document.body.style.overflow = ''; if (lastFocus && lastFocus.focus) lastFocus.focus(); } document.querySelectorAll('[data-modal]').forEach(function (btn) { btn.addEventListener('click', function () { open(btn.getAttribute('data-modal')); }); }); root.querySelectorAll('[data-close]').forEach(function (el) { el.addEventListener('click', close); }); document.addEventListener('keydown', function (e) { if (e.key === 'Escape' && root.classList.contains('open')) close(); }); } })();